#!/usr/bin/ python3
# @File    : execution_service.py
# @Time    : 2020/3/20 15:00
# @Author  : Kelvin.Ye
import time

from gevent.event import Event
from loguru import logger
from pymeter.runner import Runner as PyMeterRunner

from app import config as CONFIG
from app.extension import db
from app.extension import executor
from app.extension import socketio
from app.modules.messaging.enum import NoticeEvent
from app.modules.script.dao import element_children_dao
from app.modules.script.dao import test_element_dao
from app.modules.script.dao import test_report_dao
from app.modules.script.dao import test_worker_result_dao
from app.modules.script.dao import testplan_dao
from app.modules.script.dao import testplan_execution_collection_dao
from app.modules.script.dao import testplan_execution_dao
from app.modules.script.dao import variable_dataset_dao
from app.modules.script.enum import ElementType
from app.modules.script.enum import RunningState
from app.modules.script.enum import VariableDatasetType
from app.modules.script.enum import is_test_snippet
from app.modules.script.enum import is_worker
from app.modules.script.manager.element_component import add_flask_db_iteration_storage
from app.modules.script.manager.element_component import add_flask_db_result_storage
from app.modules.script.manager.element_component import add_flask_sio_result_collector
from app.modules.script.manager.element_component import add_variable_dataset
from app.modules.script.manager.element_loader import ElementLoader
from app.modules.script.manager.element_manager import get_case_no
from app.modules.script.manager.element_manager import get_element_node
from app.modules.script.model import TTestplanExecution
from app.modules.script.model import TTestplanExecutionCollection
from app.modules.script.model import TTestReport
from app.modules.usercenter.dao import user_dao
from app.signals.notice_reveiver import notice_signal
from app.tools.cache import executing_pymeters
from app.tools.exceptions import ServiceError
from app.tools.exceptions import TestplanInterruptError
from app.tools.identity import new_id
from app.tools.identity import new_ulid
from app.tools.localvars import get_user_no
from app.tools.service import http_service
from app.tools.validator import check_exists
from app.tools.validator import check_workspace_permission
from app.utils.flask_util import get_flask_app
from app.utils.time_util import datetime_now_by_utc8
from app.utils.time_util import microsecond_to_h_m_s
from app.utils.time_util import timestamp_now
from app.utils.time_util import timestamp_to_utc8_datetime


def create_stop_event(socket_id, result_id):
    stop_event = Event()
    executing_pymeters[socket_id] = {'result_id': result_id, 'stop_event': stop_event}
    return stop_event


def remove_stop_event(socket_id):
    executing_pymeters.pop(socket_id)


def debug_pymeter(script, socket_id, result_id):
    try:
        PyMeterRunner.start(
            [script],
            extra={'sio': socketio, 'sid': socket_id},
            throw_ex=True,
            stop_event=create_stop_event(socket_id, result_id)
        )
        socketio.emit('pymeter:completed', namespace='/', to=socket_id)
    except Exception:
        logger.exception('Exception Occurred')
        socketio.emit('pymeter:error', '脚本执行异常', namespace='/', to=socket_id)
    finally:
        remove_stop_event(socket_id)


def debug_pymeter_by_loader(loader, app, socket_id):
    result_id = new_ulid()
    try:
        socketio.emit(
            'pymeter:start',
            {'id': result_id, 'name': '加载中', 'loading': True, 'running': True},
            namespace='/',
            to=socket_id
        )
        script = loader(app, result_id)
        PyMeterRunner.start(
            [script],
            extra={'sio': socketio, 'sid': socket_id},
            throw_ex=True,
            stop_event=create_stop_event(socket_id, result_id)
        )
        socketio.emit('pymeter:completed', namespace='/', to=socket_id)
    except Exception:
        logger.exception('Exception Occurred')
        socketio.emit(
            'pymeter:result_summary',
            {'resultId': result_id, 'result': {'name': 'error', 'loading': False, 'running': False}},
            namespace='/',
            to=socket_id
        )
        socketio.emit('pymeter:error', '脚本执行异常', namespace='/', to=socket_id)
    finally:
        remove_stop_event(socket_id)


@http_service
def run_collection(req):
    # 校验空间权限
    check_workspace_permission()

    # 查询元素
    collection = test_element_dao.select_by_no(req.collectionNo)
    if not collection.ENABLED:
        raise ServiceError(msg='元素已禁用')
    if collection.ELEMENT_TYPE != ElementType.COLLECTION.value:
        raise ServiceError(msg='仅支持运行测试集合')

    # 临时存储变量
    collection_name = collection.ELEMENT_NAME

    # 定义 loader 函数
    def script_loader(app, result_id):
        with app.app_context():
            # 递归加载脚本
            script = ElementLoader(req.collectionNo, offlines=req.offlines).loads_tree()
            # 添加 socket 组件
            add_flask_sio_result_collector(
                script,
                socket_id=req.socketId,
                result_id=result_id,
                result_name=collection_name
            )
            # 添加变量组件
            add_variable_dataset(
                script,
                datasets=req.datasets,
                offlines=req.offlines,
                additional=req.variables,
                use_current=req.useCurrentValue
            )
            return script

    # 新建线程执行脚本
    executor.submit(
        debug_pymeter_by_loader,
        loader=script_loader,
        app=get_flask_app(),
        socket_id=req.socketId
    )


@http_service
def run_worker(req):
    # 校验空间权限
    check_workspace_permission()
    # 异步运行脚本
    run_testcase(
        worker_no=req.workerNo,
        socket_id=req.socketId,
        offlines=req.offlines,
        datasets=req.datasets,
        variables=req.variables,
        use_current_value=req.useCurrentValue
    )


@http_service
def run_worker_by_sampler(req):
    # 校验空间权限
    check_workspace_permission()
    # 获取用例编号
    worker_no = get_case_no(req.samplerNo)
    # 异步运行脚本
    run_testcase(
        worker_no=worker_no,
        socket_id=req.socketId,
        offlines=req.offlines,
        datasets=req.datasets,
        variables=req.variables,
        use_current_value=req.useCurrentValue
    )


def run_testcase(worker_no, socket_id, offlines, datasets, variables, use_current_value):
    # 查询元素
    worker = test_element_dao.select_by_no(worker_no)
    if not worker.ENABLED:
        raise ServiceError(msg='元素已禁用')
    if worker.ELEMENT_TYPE != ElementType.WORKER.value:
        raise ServiceError(msg='仅支持运行测试用例')

    # 获取 collectionNo
    node = element_children_dao.select_by_child(worker_no)
    if not node:
        raise ServiceError(msg='元素节点不存在')

    # 临时存储变量
    collection_no = node.PARENT_NO
    worker_name = worker.ELEMENT_NAME

    # 定义 loader 函数
    def script_loader(app, result_id):
        with app.app_context():
            # 递归加载脚本
            script = ElementLoader(collection_no, worker_no=worker_no, offlines=offlines).loads_tree()
            # 添加 socket 组件
            add_flask_sio_result_collector(
                script,
                socket_id=socket_id,
                result_id=result_id,
                result_name=worker_name
            )
            # 添加变量组件
            add_variable_dataset(
                script,
                datasets=datasets,
                offlines=offlines,
                additional=variables,
                use_current=use_current_value
            )
            return script

    # 新建线程执行脚本
    executor.submit(
        debug_pymeter_by_loader,
        loader=script_loader,
        app=get_flask_app(),
        socket_id=socket_id
    )


@http_service
def run_sampler(req):
    # 校验空间权限
    check_workspace_permission()

    # 查询元素
    sampler = test_element_dao.select_by_no(req.samplerNo)
    if not sampler.ENABLED:
        raise ServiceError(msg='元素已禁用')
    if sampler.ELEMENT_TYPE != ElementType.SAMPLER.value:
        raise ServiceError(msg='仅支持运行请求')

    # 获取 collectionNo 和 workerNo
    node = get_element_node(req.samplerNo)
    if not node:
        raise ServiceError(msg='元素节点不存在')

    # 临时存储变量
    collection_no = node.ROOT_NO
    if node.ROOT_TYPE == ElementType.SNIPPET.value:
        worker_no = None
    else:
        worker_no = node.PARENT_NO if node.PARENT_TYPE == ElementType.WORKER.value else get_case_no(node.PARENT_NO)
    sampler_name = sampler.ELEMENT_NAME

    # 定义 loader 函数
    def script_loader(app, result_id):
        with app.app_context():
            # 递归加载脚本
            script = ElementLoader(
                collection_no,
                offlines=req.offlines,
                worker_no=worker_no,
                sampler_no=req.samplerNo,
                aloneness=req.aloneness
            ).loads_tree()
            # 添加 socket 组件
            add_flask_sio_result_collector(
                script,
                socket_id=req.socketId,
                result_id=result_id,
                result_name=sampler_name
            )
            # 添加变量组件
            add_variable_dataset(
                script,
                datasets=req.datasets,
                offlines=req.offlines,
                additional=req.variables,
                use_current=req.useCurrentValue
            )
            return script

    # 新建线程执行脚本
    executor.submit(
        debug_pymeter_by_loader,
        loader=script_loader,
        app=get_flask_app(),
        socket_id=req.socketId
    )


@http_service
def run_snippet(req):
    # 校验空间权限
    check_workspace_permission()

    # 查询元素
    snippet = test_element_dao.select_by_no(req.snippetNo)
    if not snippet.ENABLED:
        raise ServiceError(msg='元素已禁用')
    if not is_test_snippet(snippet):
        raise ServiceError(msg='仅支持运行测试片段')

    # 递归加载脚本
    script = ElementLoader(req.snippetNo, offlines=req.offlines).loads_tree()

    # 添加 socket 组件
    result_id = new_ulid()
    add_flask_sio_result_collector(
        script,
        socket_id=req.socketId,
        result_id=result_id,
        result_name=snippet.ELEMENT_NAME
    )

    # 添加变量组件
    add_variable_dataset(
        script,
        datasets=req.datasets,
        offlines=req.offlines,
        additional=req.variables,
        use_current=req.useCurrentValue
    )

    # 新建线程执行脚本
    executor.submit(debug_pymeter, script, req.socketId, result_id)


@http_service
def run_offline(req):
    # 校验空间权限
    check_workspace_permission()

    # 获取离线数据
    offline = req.offlines.get(req.offlineNo)
    if not offline:
        raise ServiceError(msg='离线数据不存在')

    parent = test_element_dao.select_by_no(req.parentNo)
    worker_no = req.parentNo if is_worker(parent) else get_case_no(req.parentNo)

    # 定义 loader 函数
    def script_loader(app, result_id):
        with app.app_context():
            # 递归加载脚本
            script = ElementLoader(
                req.rootNo,
                worker_no=worker_no,
                offline_no=req.offlineNo,
                offlines=req.offlines,
                aloneness=req.aloneness
            ).loads_tree()
            # 添加 socket 组件
            add_flask_sio_result_collector(
                script,
                socket_id=req.socketId,
                result_id=result_id,
                result_name=offline['elementName']
            )
            # 添加变量组件
            add_variable_dataset(
                script,
                datasets=req.datasets,
                offlines=req.offlines,
                additional=req.variables,
                use_current=req.useCurrentValue
            )
            return script

    # 新建线程执行脚本
    executor.submit(
        debug_pymeter_by_loader,
        loader=script_loader,
        app=get_flask_app(),
        socket_id=req.socketId
    )


@http_service
def execute_testplan(req):
    return run_testplan(req.planNo, req.datasets, req.useCurrentValue)


def run_testplan(plan_no, datasets, use_current_value, check_workspace=True):
    # 查询测试计划
    testplan = testplan_dao.select_by_no(plan_no)
    check_exists(testplan, error='测试计划不存在')

    # 校验空间权限
    if check_workspace:
        check_workspace_permission(testplan.WORKSPACE_NO)

    # 查询是否有正在运行中的执行任务
    running = testplan_execution_dao.select_running_by_plan(plan_no)
    if running:
        raise ServiceError(msg='测试计划正在运行中，请执行结束后再开始新的执行')

    # 创建执行编号
    execution_no = new_id()
    # 记录测试环境
    environment = None
    for dataset_no in datasets:
        dataset = variable_dataset_dao.select_by_no(dataset_no)
        if dataset.DATASET_TYPE == VariableDatasetType.ENVIRONMENT.value:
            environment = dataset.DATASET_NAME

    # 新增执行记录
    TTestplanExecution.norecord_insert(
        PLAN_NO=plan_no,
        EXECUTION_NO=execution_no,
        EXECUTION_STATE=RunningState.WAITING.value,
        ENVIRONMENT=environment,
        TEST_PHASE=testplan.TEST_PHASE,
        SETTINGS={
            'SAVE': testplan.SETTINGS['SAVE'],
            'DELAY': testplan.SETTINGS['DELAY'],
            'ITERATIONS': testplan.SETTINGS['ITERATIONS'],
            'CONCURRENCY': testplan.SETTINGS['CONCURRENCY'],
            'NOTICE_BOTS': testplan.SETTINGS['NOTICE_BOTS'],
            'SAVE_ON_ERROR': testplan.SETTINGS['SAVE_ON_ERROR'],
            'VARIABLE_DATASETS': datasets,
            'USE_CURRENT_VALUE': use_current_value,
            'STOP_ON_ERROR_COUNT': testplan.SETTINGS['STOP_ON_ERROR_COUNT']
        }
    )

    # 新增执行脚本明细
    for collection_no in testplan.COLLECTIONS:
        TTestplanExecutionCollection.norecord_insert(
            EXECUTION_NO=execution_no,
            COLLECTION_NO=collection_no,
            RUNNING_STATE=RunningState.WAITING.value
        )

    # 新增测试报告
    report_no = None
    if testplan.SETTINGS['SAVE']:
        report_no = new_id()
        TTestReport.norecord_insert(
            WORKSPACE_NO=testplan.WORKSPACE_NO,
            PLAN_NO=testplan.PLAN_NO,
            EXECUTION_NO=execution_no,
            REPORT_NO=report_no,
            REPORT_NAME=testplan.PLAN_NAME
        )

    # 临时存储变量
    collections = testplan.COLLECTIONS
    save = testplan.SETTINGS.get('SAVE')
    delay = testplan.SETTINGS.get('DELAY')
    iterations = testplan.SETTINGS.get('ITERATIONS')
    notice_bots = testplan.SETTINGS.get('NOTICE_BOTS')
    save_on_error = testplan.SETTINGS.get('SAVE_ON_ERROR')

    # 异步函数
    def start(app):
        try:
            with app.app_context():
                start_testplan(
                    app,
                    collections,
                    datasets,
                    use_current_value,
                    execution_no,
                    report_no,
                    iterations,
                    delay,
                    save,
                    save_on_error,
                    notice_bots
                )
        except Exception:
            logger.exception(f'执行编号:[ {execution_no} ] 执行异常')
            with app.app_context():
                try:
                    testplan_execution_dao.update_execution_state(execution_no, RunningState.ERROR.value)
                except Exception:
                    logger.exception(f'执行编号:[ {execution_no} ] 执行异常')

    # 先提交事务，防止新线程查询计划时拿不到
    db.session.commit()
    # 异步执行脚本
    executor.submit(start, get_flask_app())

    return {'executionNo': execution_no, 'total': len(collections)}


def start_testplan(
        app,
        collections,
        datasets,
        use_current_value,
        execution_no,
        report_no,
        iterations,
        delay,
        save,
        save_on_error,
        notice_bots
):
    logger.info(f'执行编号:[ {execution_no} ] 开始执行测试计划')
    # 记录开始时间
    start_time = timestamp_now()
    # 查询执行记录
    execution = testplan_execution_dao.select_by_no(execution_no)
    # 更新运行状态
    execution.norecord_update(
        EXECUTION_STATE=RunningState.RUNNING.value if save else RunningState.ITERATING.value,
        START_TIME=timestamp_to_utc8_datetime(start_time)
    )
    db.session.commit()  # 这里要实时更新

    if save:
        if save_on_error:
            start_testplan_by_error_report(
                app,
                collections,
                datasets,
                use_current_value,
                execution_no,
                report_no
            )
        else:
            start_testplan_by_report(
                app,
                collections,
                datasets,
                use_current_value,
                execution_no,
                report_no
            )
    else:
        start_testplan_by_loop(
            app,
            collections,
            datasets,
            use_current_value,
            execution_no,
            iterations,
            delay
        )

    # 记录结束时间
    end_time = timestamp_now()
    # 计算耗时
    elapsed_time = int(end_time * 1000) - int(start_time * 1000)

    report = None
    if report_no:
        # 更新报告的开始时间、结束时间和耗时
        report = test_report_dao.select_by_no(report_no)
        report.norecord_update(
            ELAPSED_TIME=elapsed_time,
            START_TIME=timestamp_to_utc8_datetime(start_time),
            END_TIME=timestamp_to_utc8_datetime(end_time)
        )
        db.session.commit()  # 这里要实时更新

    # 重新查询执行记录
    execution = testplan_execution_dao.select_by_no(execution_no)
    # 更新运行状态，仅运行中和迭代中才更新为已完成
    if execution.EXECUTION_STATE in (RunningState.RUNNING.value, RunningState.ITERATING.value):
        execution.norecord_update(
            EXECUTION_STATE=RunningState.COMPLETED.value,
            ELAPSED_TIME=elapsed_time,
            END_TIME=timestamp_to_utc8_datetime(end_time)
        )
        db.session.commit()  # 这里要实时更新

    # 结果通知
    if notice_bots:
        logger.info(f'执行编号:[ {execution_no} ] 发送结果通知')
        for bot_no in notice_bots:
            notice_signal.send(
                bot_no=bot_no,
                event=NoticeEvent.TESTPLAN_EXECUTION_COMPLETED.value,
                markdown=get_notice_message(execution, report)
            )
    # 执行完毕
    logger.info(f'执行编号:[ {execution_no} ] 计划执行完成')


def get_notice_message(execution: TTestplanExecution, report: TTestReport):
    testplan = testplan_dao.select_by_no(execution.PLAN_NO)
    user = user_dao.select_by_no(execution.CREATED_BY)
    if report:
        elapsed_time = microsecond_to_h_m_s(report.ELAPSED_TIME)
        success_count = test_worker_result_dao.count_by_report_and_success(report.REPORT_NO, True)
        failure_count = test_worker_result_dao.count_by_report_and_success(report.REPORT_NO, False)
        report_url = f'{CONFIG.BASE_URL}/script/report?reportNo={report.REPORT_NO}'
        return (
            f'# 测试计划执行完毕\n'
            f'>**计划名称：**<font color="comment">{testplan.PLAN_NAME}</font>\n'
            f'>**执行环境：**<font color="comment">{execution.ENVIRONMENT}</font>\n'
            f'>**执行人：**<font color="comment">{user.USER_NAME}</font>\n'
            f'>**耗时：**<font color="comment">{elapsed_time}</font>\n'
            f'>**成功：**<font color="info">{success_count}</font>\n'
            f'>**失败：**<font color="warning">{failure_count}</font>\n\n'
            f'[点击这里查看测试报告]({report_url})'
        )
    else:
        elapsed_time = microsecond_to_h_m_s(execution.ELAPSED_TIME)
        success_count = testplan_execution_collection_dao.sum_success_count_by_execution(execution.EXECUTION_NO)
        failure_count = testplan_execution_collection_dao.sum_failure_count_by_execution(execution.EXECUTION_NO)
        return (
            f'# 测试计划执行完毕\n'
            f'>**计划名称：**<font color="comment">{testplan.PLAN_NAME}</font>\n'
            f'>**执行环境：**<font color="comment">{execution.ENVIRONMENT}</font>\n'
            f'>**执行人：**<font color="comment">{user.USER_NAME}</font>\n'
            f'>**总共耗时：**<font color="comment">{elapsed_time}</font>\n'
            f'>**迭代次数：**<font color="comment">{execution.ITER_COUNT} 次</font>\n'
            f'>**成功迭代：**<font color="info">{success_count} 次</font>\n'
            f'>**失败迭代：**<font color="warning">{failure_count} 次</font>'
        )


def start_testplan_by_loop(
        app,
        collections,
        datasets,
        use_current_value,
        execution_no,
        iterations,
        delay
):
    """循环运行测试计划"""
    # 批量解析脚本并临时存储
    logger.info(f'执行编号:[ {execution_no} ] 开始批量加载脚本')
    scripts = {}
    for collection_no in collections:
        # 加载脚本
        collection = ElementLoader(collection_no, exclude_skip=True).loads_tree()
        if not collection:
            logger.warning(
                f'执行编号:[ {execution_no} ] 集合编号:[ {collection_no} ] 脚本为空或脚本已禁用，跳过当前脚本'
            )
            continue
        # 添加自定义变量组件
        add_variable_dataset(collection, datasets=datasets, use_current=use_current_value)
        # 添加迭代记录器组件
        add_flask_db_iteration_storage(collection, execution_no, collection_no)
        # 存储加载后的脚本，不需要每次迭代都重新加载一遍
        scripts[collection_no] = collection

    # 批量更新计划项目的运行状态至 RUNNING
    logger.info(f'执行编号:[ {execution_no} ] 脚本加载完成，开始运行测试计划')
    testplan_execution_collection_dao.update_running_state_by_execution(
        execution_no,
        state=RunningState.ITERATING.value
    )
    db.session.commit()  # 这里要实时更新

    try:
        # 循环运行
        for i in range(iterations):
            logger.info(f'执行编号:[ {execution_no} ] 开始第[ {i+1} ]次迭代')
            # 记录迭代次数
            execution = testplan_execution_dao.select_by_no(execution_no)
            execution.norecord_update(ITER_COUNT=execution.ITER_COUNT + 1)
            db.session.commit()  # 这里要实时更新
            # 延迟迭代
            if delay and i > 0:
                logger.info(f'间隔等待{delay}ms')
                time.sleep(float(delay / 1000))
            # 顺序执行脚本
            for collection_no, collection in scripts.items():
                # 检查是否需要中断执行
                execution = testplan_execution_dao.select_by_no(execution_no)
                if execution.INTERRUPT:
                    raise TestplanInterruptError()

                # 异步函数
                def start(app):
                    try:
                        logger.info(
                            f'执行编号:[ {execution_no} ] 集合名称:[ {collection["name"]} ] 第[ {i} ]次开始执行脚本'
                        )
                        PyMeterRunner.start([collection], throw_ex=True)
                    except Exception:
                        logger.exception(f'执行编号:[ {execution_no} ] 集合编号:[ {collection_no} ] 脚本执行异常')
                        with app.app_context():
                            try:
                                script = testplan_execution_collection_dao.select_by_execution_and_collection(
                                    execution_no,
                                    collection_no
                                )
                                script.norecord_update(ERROR_COUNT=script.ERROR_COUNT + 1)
                            except Exception:
                                logger.exception(f'执行编号:[ {execution_no} ] 集合编号:[ {collection_no} ] 更新异常')

                task = executor.submit(start, app)  # 异步执行脚本
                task.result()  # 阻塞等待脚本执行完成
    except TestplanInterruptError:
        logger.info(f'执行编号:[ {execution_no} ] 用户中断迭代')
    except Exception:
        logger.exception(f'执行编号:[ {execution_no} ] 运行异常')
        testplan_execution_collection_dao.update_running_state_by_execution(
            execution_no,
            state=RunningState.ERROR.value
        )

    # 批量更新执行脚本的运行状态至 COMPLETED
    testplan_execution_collection_dao.update_running_state_by_execution(
        execution_no,
        state=RunningState.COMPLETED.value
    )
    db.session.commit()  # 这里要实时更新
    logger.info(f'执行编号:[ {execution_no} ] 计划迭代完成')


def start_testplan_by_report(
        app,
        collections,
        datasets,
        use_current_value,
        execution_no,
        report_no
):
    """运行测试计划并保存测试结果"""
    try:
        # 顺序执行脚本
        for collection_no in collections:
            # 检查是否需要中断执行
            execution = testplan_execution_dao.select_by_no(execution_no)
            if execution.INTERRUPT:
                raise TestplanInterruptError()
            # 查询执行脚本
            script = testplan_execution_collection_dao.select_by_execution_and_collection(execution_no, collection_no)
            # 更新脚本运行状态
            script.norecord_update(RUNNING_STATE=RunningState.RUNNING.value)
            db.session.commit()  # 这里要实时更新
            # 加载脚本
            collection = ElementLoader(collection_no, exclude_skip=True).loads_tree()
            if not collection:
                logger.warning(
                    f'执行编号:[ {execution_no} ] 集合编号:[ {collection_no} ] 脚本为空或脚本已禁用，跳过当前脚本'
                )
            # 添加自定义变量组件
            add_variable_dataset(collection, datasets=datasets, use_current=use_current_value)
            # 添加报告存储器组件
            add_flask_db_result_storage(collection, report_no, collection_no)

            # 异步函数
            def start(app):
                try:
                    logger.info(f'执行编号:[ {execution_no} ] 集合名称:[ {collection["name"]} ] 开始执行脚本')
                    PyMeterRunner.start([collection], throw_ex=True)
                except Exception:
                    logger.exception(f'执行编号:[ {execution_no} ] 集合编号:[ {collection_no} ] 脚本执行异常')
                    with app.app_context():
                        try:
                            script = testplan_execution_collection_dao.select_by_execution_and_collection(
                                execution_no,
                                collection_no
                            )
                            script.norecord_update(RUNNING_STATE=RunningState.ERROR.value)
                        except Exception:
                            logger.exception(f'执行编号:[ {execution_no} ] 集合编号:[ {collection_no} ] 更新异常')
            task = executor.submit(start, app)  # 异步执行脚本
            task.result()  # 阻塞等待脚本执行完成
            # 更新脚本运行状态
            script.norecord_update(RUNNING_STATE=RunningState.COMPLETED.value)
            db.session.commit()  # 这里要实时更新
            logger.info(f'执行编号:[ {execution_no} ] 集合名称:[ {collection["name"]} ] 脚本执行完成')
    except TestplanInterruptError:
        logger.info(f'执行编号:[ {execution_no} ] 用户中断迭代')
    except Exception:
        logger.exception(f'执行编号:[ {execution_no} ] 运行异常')
        testplan_execution_collection_dao.update_running_state_by_execution(
            execution_no,
            state=RunningState.ERROR.value
        )


def start_testplan_by_error_report(
        app,
        collections,
        datasets,
        use_current_value,
        execution_no,
        report_no
):
    """运行测试计划，但仅保存失败的测试结果"""
    ...


@http_service
def interrupt_testplan(req):
    # 查询执行记录
    execution = testplan_execution_dao.select_by_no(req.executionNo)
    check_exists(execution, error='执行记录不存在')

    # 查询测试计划
    testplan = testplan_dao.select_by_no(execution.PLAN_NO)
    check_exists(testplan, error='测试计划不存在')

    # 校验空间权限
    check_workspace_permission(testplan.WORKSPACE_NO)

    # 标记执行中断
    execution.update(
        INTERRUPT=True,
        INTERRUPT_BY=get_user_no(),
        INTERRUPT_TIME=datetime_now_by_utc8(),
        EXECUTION_STATE=RunningState.INTERRUPTED.value
    )
